package com.cat.util.join;

import cn.hutool.core.bean.BeanPath;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @program: eunomia
 * @description: 一个工具类用于对象集合的属性用连接方式赋值
 * @author: wangchangquan/cat
 * @create: 2018-08-21-12:49
 **/
public class Join {

    /**
     * 解决程序使用in的方式来连接外表数据
     *
     * @param data              左边的数据集合
     * @param leftPropExp       左边数据连接的属性
     * @param leftRecivePropExp 左边数据接收右边连接到对象的属性
     * @param right             右边的数据源对象
     * @param rightMethod       右边的数据来源方法
     * @param rightPropExp      右边的数据的属性
     * @param converter         右边类转换为左边属性类的转换器
     * @param <T>               左边数据类型
     * @param <S>               右边返回数据类型
     * @param <D>               左边接收数据类型
     * @return
     */
    public static <T, S, D> Collection<T> leftOuter(
            Collection<T> data,
            String leftPropExp,
            String leftRecivePropExp,
            Object right,
            String rightMethod,
            String rightPropExp,
            Converter<S, D> converter
    ) {
        // 验证参数
        if (!validate(data, right, leftPropExp, leftRecivePropExp, rightMethod)) {
            return data;
        }

        // 获取左边的id映射
        Map map = leftMap(data, leftPropExp);

        // 如果需要连接的数据长度大于0则才执行数据连接
        if (map.size() > 0) {
            // 调用方法获取右边数据
            Collection rightData = getRightData(right, rightMethod, map.keySet());

            // 将右边数据通过转换设置到左边的数据集中
            setRightDataToLeftData(map, leftRecivePropExp, rightData, rightPropExp, converter);
        }

        return data;
    }


    /**
     * 解决程序使用in的方式来连接外表数据
     *
     * @param data    左边的数据集合
     * @param clazz   记录着注解的类
     * @param context 上下文
     * @param props   限定转换的属性
     * @param <T>     左边数据类型
     * @return
     */
    public static <T> Collection<T> leftOuter(Collection data, Class<?> clazz, Object context, String... props) {

        // 获取注解参数
        List<Map<String, Object>> params = getParams(clazz, props, context);

        // 调用接口将数据设置到左边
        for (Map<String, Object> param : params) {
            data = (Collection<T>) leftOuter(
                    data,
                    (String) param.get(Parameter.PARAM_LEFT_PROP_EXP),
                    (String) param.get(Parameter.PARAM_LEFT_RECIVE_PROP_EXP),
                    param.get(Parameter.PARAM_RIGHT),
                    (String) param.get(Parameter.PARAM_RIGHT_METHOD),
                    (String) param.get(Parameter.PARAM_RIGHT_PROP_EXP),
                    (Converter) param.get(Parameter.PARAM_CONVERTER)
            );
        }
        return data;
    }

    /**
     * 解决程序使用in的方式来连接外表数据
     *
     * @param data    左边的数据集合
     * @param context 上下文
     * @param props   限定转换的属性
     * @param <T>     左边数据类型
     * @return
     */
    public static <T> Collection<T> leftOuter(Collection data, Object context, String... props) {

        // 通过数据集获取类并获取注解参数
        List<Map<String, Object>> params = getParams(getClassByData(data), props, context);

        // 调用接口将数据设置到左边
        for (Map<String, Object> param : params) {
            data = (Collection<T>) leftOuter(
                    data,
                    (String) param.get(Parameter.PARAM_LEFT_PROP_EXP),
                    (String) param.get(Parameter.PARAM_LEFT_RECIVE_PROP_EXP),
                    param.get(Parameter.PARAM_RIGHT),
                    (String) param.get(Parameter.PARAM_RIGHT_METHOD),
                    (String) param.get(Parameter.PARAM_RIGHT_PROP_EXP),
                    (Converter) param.get(Parameter.PARAM_CONVERTER)
            );
        }
        return data;
    }

    private static Class<?> getClassByData(Collection data) {
        Object o = null;
        if (Objects.isNull(data) || data.isEmpty() || Objects.isNull((o = data.stream().findFirst().get()))) {
            return null;
        }
        return o.getClass();
    }


    private static List<Map<String, Object>> getParams(Class<?> clazz, String[] props, Object context) {


        List<Map<String, Object>> params = CollUtil.newArrayList();

        if (Objects.isNull(clazz)) {
            return params;
        }

        if (Objects.isNull(props) || props.length == 0) {
            Field[] fields = ReflectUtil.getFields(clazz);
            for (Field field : fields) {
                recursionField(params, Parameter.DEFAULT_LEFT_PREFIX, field, context);
            }
        } else {
            for (String prop : props) {
                recursionField(params, clazz, prop, context);
            }
        }

        return params;
    }

    private static void recursionField(
            List<Map<String, Object>> params,
            String prefix,
            Field field,
            Object context
    ) {
        Map<String, Object> param = getParam(prefix, field, context);
        addNonNullToList(params, param);

        if (Objects.nonNull(param) && (boolean) param.get(Parameter.PARAM_RECURSION)) {
            Class<?> type = field.getType();
            if (ClassUtil.isBasicType(type) || type == String.class) {
                return;
            }
            Field[] fields = ReflectUtil.getFields(type);
            for (Field f : fields) {
                recursionField(params, (String) param.get(Parameter.PARAM_NEXT_LEFT_PREFIX), f, context);
            }
        }
    }

    private static void recursionField(
            List<Map<String, Object>> params,
            Class<?> clazz,
            String prop,
            Object context
    ) {

        String[] props = StringUtils.split(prop, Parameter.DEFAULT_LEFT_PREFIX_SPLITOR);
        Field field = null;
        if (Objects.nonNull(props)) {
            if (props.length > 1) {
                Class<?> ft = clazz;
                for (String p : props) {
                    field = ReflectUtil.getField(ft, p);
                    if (Objects.isNull(field)) {
                        return;
                    }
                    ft = field.getType();
                }
                recursionField(
                        params,
                        StringUtils.join(props, Parameter.DEFAULT_LEFT_PREFIX_SPLITOR, 0, props.length - 1) + Parameter.DEFAULT_LEFT_PREFIX_SPLITOR,
                        field,
                        context);
            } else if (props.length == 1){
                field = ReflectUtil.getField(clazz, props[0]);
                if (Objects.nonNull(field)) {
                    recursionField(
                            params,
                            Parameter.DEFAULT_LEFT_PREFIX,
                            field,
                            context);
                }
            }
        }
    }

    private static void addNonNullToList(List list, Object o) {
        if (Objects.nonNull(o)) {
            list.add(o);
        }
    }

    private static Map<String, Object> getParam(String leftPrefix, Field field, Object context) {
        return Parameter.parseLeftOuter(leftPrefix, field, context);
    }

    private static boolean validate(Collection data, Object join, String propExp, String recivePropExp, String joinMethod) {
        return !(Objects.isNull(data)
                || Objects.isNull(join)
                || StringUtils.isBlank(propExp)
                || StringUtils.isBlank(recivePropExp)
                || StringUtils.isBlank(joinMethod));
    }

    private static void setRightDataToLeftData(Map map, String recivePropExp, Collection rightData, String joinPropExp, Converter converter) {
        BeanPath joinPropBeanPath = BeanPath.create(joinPropExp);
        BeanPath recivePropExpBeanPath = BeanPath.create(recivePropExp);
        rightData.forEach(d -> {
            setListDataOfMap(map, recivePropExpBeanPath, joinPropBeanPath.get(d), converter.convert(d));
        });
    }

    private static void setListDataOfMap(Map map, BeanPath rpebp, Object p, Object v) {
        List l = (List) map.get(p);
        if (Objects.nonNull(l)) {
            l.forEach(d -> {
                if (Objects.nonNull(d)) {
                    rpebp.set(d, v);
                }
            });
        }
    }

    private static Map leftMap(Collection data, String propExp) {
        Map map = CollUtil.newHashMap(data.size());
        BeanPath propBeanPath = BeanPath.create(propExp);
        data.forEach(d -> {
            if (Objects.nonNull(d)) {
                Object property = propBeanPath.get(d);
                if (Objects.nonNull(property)) {
                    addRequireListToMap(map, property, d);
                }
            }
        });
        return map;
    }

    private static void addRequireListToMap(Map map, Object p, Object o) {
        List l = null;
        if (Objects.isNull((l = (List) map.get(p)))) {
            l = CollUtil.newLinkedList();
            map.put(p, l);
        }
        l.add(o);
    }

    private static Collection getRightData(Object join, String joinMethod, Collection lefts) {

        Collection rightData = null;
        Class<?> aClass = join.getClass();
        Method cm = ReflectUtil.getMethod(aClass, joinMethod, Collection.class);
        if (Objects.nonNull(cm)) {
            rightData = (Collection) ReflectUtil.invoke(join, cm, lefts);
        } else {
            Method sm = ReflectUtil.getMethod(aClass, joinMethod, Set.class);
            if (Objects.nonNull(sm)) {
                rightData = (Collection) ReflectUtil.invoke(join, sm, lefts);
            } else {
                Method lm = ReflectUtil.getMethod(aClass, joinMethod, List.class);
                if (Objects.nonNull(lm)) {
                    rightData = (Collection) ReflectUtil.invoke(join, lm, CollUtil.newArrayList(lefts));
                }
            }
        }
        return Objects.isNull(rightData) ? Collections.EMPTY_LIST : rightData;
    }
}
